using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.IO;
using Microsoft.Modeling.StoryboardDesigner.DslPackage.Extensions.Common;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Diagrams;

namespace Microsoft.Modeling.StoryboardDesigner.DslPackage
{
    internal partial class StoryboardDesignerCommandSet
    {
        #region Context menu handling

        /// <summary>
        /// Returns a flag indicating whether the "Move to new storyboard"
        /// context menu should be displayed.
        /// </summary>
        /// <remarks>Enable the command when at least one concept is selected</remarks>
        private void OnStatusMoveToNewStoryboard(object sender, EventArgs args)
        {
            MenuCommand command = sender as MenuCommand;

            if(command == null)
            {
                return;
            }

            command.Enabled = false;

            foreach(PresentationElement pel in this.CurrentDocumentSelection)
            {
                if(pel.ModelElement != null && pel.ModelElement is StoryboardElement)
                {
                    command.Enabled = true;
                    return;
                }
            }
        }

        /// <summary>
        /// Context menu handler - called when "move to new storyboard"
        /// is clicked.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void OnMoveToNewStoryboard(object sender, EventArgs args)
        {
            ModelElement[] arr = new ModelElement[this.CurrentDocumentSelection.Count];
            this.CurrentDocumentSelection.CopyTo(arr, 0);
            List<ModelElement> selectedElements = new List<ModelElement>(arr);

            // Create and save a new diagram
            string newFileName = CreateNewStoryboardFromSelection(selectedElements);

            StoryboardDiagram diagram = (StoryboardDiagram)this.CurrentDocView.CurrentDiagram;
            Debug.Assert(diagram != null, "Error - diagram is null");

            using(Transaction t = diagram.Store.TransactionManager.BeginTransaction("Amend existing diagram"))
            {
                // Create a new marker element to replace the shapes being moved
                StoryboardElement marker = CreateNewMarkerElement(
                    diagram,
                    selectedElements,
                    newFileName,
                    t);

                //Amend the existing model
                ReplaceSelectionWithMarker(
                    selectedElements,
                    marker);

                t.Commit();
            }

            // Add the new file the the project, but don't open it
            AddFileToProject(newFileName, false);
        }

        /// <summary>
        /// Adds the specified file to the first project
        /// and (optionally) opens it.
        /// </summary>
        /// <param name="fileName"></param>
        /// <param name="openFile"></param>
        private void AddFileToProject(string fileName, bool openFile)
        {
            // Get a ref to the current DTE object
            EnvDTE.DTE dte = this.ServiceProvider.GetService<EnvDTE.DTE>();
            Debug.Assert(dte != null, "Error - could not get a ref to the current DTE");

            EnvDTE.ProjectItem prjItem = dte.ItemOperations.AddExistingItem(fileName);

            if(openFile)
            {
                prjItem.Open(EnvDTE.Constants.vsViewKindDesigner);
                prjItem.Document.ActiveWindow.Activate();
            }
        }

        /// <summary>
        /// Returns the top-most parent shape of the supplied shape
        /// that isn't the diagram.
        /// </summary>
        /// <remarks>Usefully when right-clicking on compartments - we
        /// are usually interested in what contains the compartment, not
        /// in the compartment itself.</remarks>
        private static ShapeElement GetTopMostParent(ShapeElement shape)
        {
            while(shape != null && !(shape.ParentShape is Diagram))
            {
                shape = shape.ParentShape;
            }

            return shape;
        }

        #endregion

        #region Create a new diagram
        /// <summary>
        /// Returns a list of the model elements and presenation elements
        /// that are related to the items in the supplied list e.g.
        /// Pels for any mels, mels for any pels, and mels and pels for any
        /// contained elements.
        /// </summary>
        /// <param name="selectedElements"></param>
        /// <returns></returns>
        private static IList<ModelElement> GetRelatedMelsAndPels(IList<ModelElement> selectedElements)
        {
            // Keep everything in the supplied selection
            List<ModelElement> keepList = new List<ModelElement>(selectedElements);

            // Loop through the supplied selection
            foreach(ModelElement item in selectedElements)
            {
                ModelElement mel = null;
                PresentationElement pel = item as PresentationElement;

                // If this is a presentation element, then add the model element
                if(pel != null)
                {
                    mel = pel.ModelElement;
                    if(mel != null) { keepList.Add(mel); }
                }
                else
                {
                    mel = item;

                    // Not a pel, so assume it is a mel - add any pels
                    LinkedElementCollection<PresentationElement> pels = PresentationViewsSubject.GetPresentation(mel);
                    keepList.AddRange(pels.ToArray());
                }
            }

            return keepList;
        }

        /// <summary>
        /// Create a new storyboard containing only the selected presentation elements
        /// and the model elements that they represent.
        /// </summary>
        /// <returns>The full name of the new storyboard file</returns>
        /// <remarks>The algorithm for creating a new diagram is as follows:
        /// * begin a transaction
        /// * delete all of the shapes and models elements from the current
        ///     diagram EXCEPT those that have been selected
        /// * serialize the store to a new file
        /// * do NOT commit the transaction, so the deleted elements are reinstated
        /// * add the new file to the project
        /// ASSUMPTIONS:
        /// * there is only one diagram in the store.</remarks>
        private string CreateNewStoryboardFromSelection(IList<ModelElement> selectedElements)
        {
            string modelFileName = string.Empty;

            using(Transaction t = this.CurrentDocData.Store.TransactionManager.BeginTransaction("CreateNewDiagram"))
            {
                // Delete all of the items that are not in the selection
                DeleteElementsNotInSelection(this.CurrentDocView.CurrentDiagram, selectedElements);

                //Reposition the remaining elements nearer the top left of the diagram
                RepositionElementsInSelection(this.CurrentDocView.CurrentDiagram, selectedElements);

                StoryboardDiagram diagram = this.CurrentStoryboardDesignerDocView.Diagram as StoryboardDiagram;
                Storyboard model = diagram.ModelElement as Storyboard;

                // Save the model and diagram
                modelFileName = GetUniqueFileName(
                    Path.GetDirectoryName(this.CurrentDocData.FileName),
                    Properties.Resources.ModelFileName);

                string diagramFileName = modelFileName + Properties.Resources.ModelExtension;

                StoryboardDesignerSerializationHelper.Instance.SaveModelAndDiagram(
                    new SerializationResult(),
                    model,
                    modelFileName,
                    diagram,
                    diagramFileName);

                // Do NOT commit the transaction
            }

            return modelFileName;
        }

        /// <summary>
        /// Reposition the elements in the selection so that 
        /// they are at the top left of the diagram.
        /// </summary>
        /// <param name="diagram"></param>
        /// <param name="selectedElements"></param>
        private void RepositionElementsInSelection(
            Diagram diagram,
            IList<ModelElement> selectedElements)
        {
            return;

            //// TODO:
            //// Work out how much we need to reposition the shapes by
            //// i.e. get the x-pos of the top-most pel and the 
            //// y-pos of the left-most pel

            //double minX = double.MaxValue;
            //double minY = double.MaxValue;

            //foreach (PresentationElement pel in selectedElements)
            //{
            //    if (pel.ModelElement != null && pel.ModelElement is Concept)
            //    {
            //        ShapeElement sel = pel as ShapeElement;
            //        if (sel != null)
            //        {
            //            minX = Math.Min(sel.AbsoluteBoundingBox.Left, minX);
            //            minY = Math.Min(sel.AbsoluteBoundingBox.Top, minY);
            //        }
            //    }
            //}


            //// Move each of the concept shapes by the specified amount
            //foreach (PresentationElement pel in selectedElements)
            //{
            //    if (pel.ModelElement != null && pel.ModelElement is Concept)
            //    {
            //        NodeShape sel = pel as NodeShape;
            //        if (sel != null)
            //        {
            //            RectangleD rect = sel.Bounds;
            //            sel.Bounds = new RectangleD(
            //                rect.Left - minX,
            //                rect.Top - minY,
            //                rect.Width,
            //                rect.Height);
            //        }
            //    }
            //}

            // TODO: reposition all of the links
        }

        /// <summary>
        /// Deletes all of the MEL and PEL elements from the 
        /// supplied diagram except those PEL elements that are 
        /// in the supplied collection, and the MEL elements 
        /// that they represent.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="selectedShapes"></param>
        private void DeleteElementsNotInSelection(
            Diagram diagram,
            IList<ModelElement> selectedElements)
        {
            // Build a list of all of the model elements that 
            // we want to keep
            IList<ModelElement> keepList = GetRelatedMelsAndPels(selectedElements);

            // Make a copy of the virtual collection - otherwise we will change
            // the collection when we delete items.
            ShapeElement[] allPels = new ShapeElement[diagram.NestedChildShapes.Count];
            diagram.NestedChildShapes.CopyTo(allPels, 0);

            // Now delete everything except links from the diagram.
            // Why leave the links and link shapes behind? It means that the user
            // doesn't have to explicitly select the link for them to be copied - it
            // the user selects the concept at either end, then any links between them
            // will be preserved.
            // There may well be orphaned links - not certain how these would normally
            // be cleaned up. However, the serialized file doesn't contain any orphans.
            foreach(PresentationElement pel in allPels)
            {
                // Delete the mel
                if(pel.ModelElement != null &&
                    !keepList.Contains(pel.ModelElement) &&
                    !(pel.ModelElement is ElementLink))
                {
                    pel.ModelElement.Delete();
                }

                if(!keepList.Contains(pel) &&
                    !(pel is BinaryLinkShape))
                {
                    // Delete the pel
                    pel.Delete();
                }
            }
        }

        /// <summary>
        /// Returns a unique name for a new storyboard file
        /// </summary>
        /// <returns></returns>
        private static string GetUniqueFileName(string path, string root)
        {
            int count = 1;
            string fileName = Path.Combine(path, root + count.ToString());
            fileName = Path.ChangeExtension(fileName, Constants.DesignerFileExtension);

            while(File.Exists(fileName))
            {
                count++;
                fileName = Path.Combine(path, root + count.ToString());
                fileName = Path.ChangeExtension(fileName, Constants.DesignerFileExtension);
            }

            return fileName;
        }
        #endregion

        #region Amend existing diagram
        /// <summary>
        /// Creates a new element on the supplied diagram that will
        /// be used to replace a selection that is being replaced.
        /// </summary>
        /// <remarks>Returns the new element</remarks>
        private static StoryboardElement CreateNewMarkerElement(
            StoryboardDiagram diagram,
            IList<ModelElement> selectedElements,
            string storyboardFileName,
            Transaction transaction)
        {
            // Sanity checks
            Debug.Assert(diagram != null, "Error - diagram is null");
            Debug.Assert(selectedElements != null, "Error - selectedPels is null");
            Debug.Assert(transaction != null, "Error - not in a transaction");

            // *************************
            // Create the new element and set its properties
            // *************************
            List<ModelElement> melsInSelection = GetMelsInSelection(selectedElements);

            // Decide the new type of MEL to create
            Guid newDomainClassId = GetNewDomainClassType(melsInSelection);
            StoryboardElement newRef = diagram.Partition.ElementFactory.CreateElement(newDomainClassId) as StoryboardElement;
            Storyboard model = diagram.ModelElement as Storyboard;

            // Sanity check
            Debug.Assert(model != null, "Error - Storyboard for this supplied diagram is null");

            // Add it to the model
            model.Elements.Add(newRef);

            // Set the file name
            newRef.LinkedFile = storyboardFileName;

            //TODO: we are assuming that this name is unique - might not be
            newRef.Name = Path.GetFileNameWithoutExtension(storyboardFileName);

            //**********************************
            // Set the position of the new item
            //**********************************
            // NB reuses GJ's positioning code

            // Get the location of the first selected concept
            NodeShape pel = GetFirstPelInSelection(selectedElements);
            RectangleD bounds;
            if(pel == null)
            {
                bounds = new RectangleD(0, 0, 0, 0);
            }
            else
            {
                bounds = pel.AbsoluteBounds;
            }

            return newRef;
        }

        /// <summary>
        /// Amends the supplied diagram by replacing the selected
        /// elements with a single "StoryboardRef" marker concept.
        /// </summary>
        /// <param name="diagram"></param>
        /// <param name="iCollection"></param>
        private static void ReplaceSelectionWithMarker(
            IList<ModelElement> selectedElements,
            StoryboardElement markerElement)
        {
            // Sanity checks
            Debug.Assert(selectedElements != null, "Error - selectedPels is null");
            Debug.Assert(markerElement != null, "Error - markerElement is null");
            Debug.Assert(markerElement.Store.TransactionActive, "Error - not in a transaction");

            // Get a list of the mels that the pels link to
            IList<ModelElement> deleteList = GetRelatedMelsAndPels(selectedElements);
            List<ModelElement> melsInSelection = GetMelsInSelection(deleteList);

            //*********************************************
            // Duplicate any links in to the marker element
            //*********************************************
            CreateLinksToStoryboardRef(markerElement.Storyboard, markerElement, melsInSelection);

            // *************************
            // Delete the selected items
            // *************************
            DeleteElementsInSelection(deleteList);
        }

        /// <summary>
        /// Determines the type of the new element that should be created
        /// when moving shapes to a new diagram.
        /// </summary>
        /// <remarks>If all of the elements are of the same type, then a new
        /// element of that type will be created. Otherwise, a new
        /// StoryboardElement will be created.</remarks>
        private static Guid GetNewDomainClassType(List<ModelElement> melsInSelection)
        {
            // Sanity check
            Debug.Assert(melsInSelection != null, "Error - melsInSelection should not be null");
            Debug.Assert(melsInSelection.Count > 0, "Error - collection is empty");

            // Be defensive - if in doubt, return StoryboardElement.
            if(melsInSelection == null || melsInSelection.Count == 0)
            {
                return StoryboardElement.DomainClassId;
            }

            // Compare against the domain class of the first element.
            Guid tempDomainClassId = melsInSelection[0].GetDomainClass().Id;

            foreach(ModelElement modelElement in melsInSelection)
            {
                // Only interested in StoryboardElement and its subtypes
                if(modelElement is StoryboardElement && tempDomainClassId != modelElement.GetDomainClass().Id)
                {
                    // Not all of the same type - return StoryboardElement
                    return StoryboardElement.DomainClassId;
                }
            }

            // All of the same type, so return that
            return tempDomainClassId;
        }

        /// <summary>
        /// Delete all of the model elements that are represented 
        /// by the supplied presentation elements
        /// </summary>
        /// <param name="selectedPels"></param>
        private static void DeleteElementsInSelection(IList<ModelElement> selectedElements)
        {
            foreach(ModelElement el in selectedElements)
            {
                PresentationElement pel = el as PresentationElement;

                if(pel != null && pel.ModelElement != null && !(pel is Diagram))
                {
                    pel.ModelElement.Delete();
                }
            }
        }

        /// <summary>
        /// Creates links between marker sets and the
        /// </summary>
        private static void CreateLinksToStoryboardRef(
            Storyboard model,
            StoryboardElement newRef,
            List<ModelElement> selectedMels)
        {
            // List of elements that we have linked the new
            // storyboard reference to. Used to stop duplicate
            // relationships from being created.
            List<ModelElement> elementsLinkedTo = new List<ModelElement>();
            // Add the model to the list so that we don't try to create 
            // another link to it
            elementsLinkedTo.Add(model);

            // Loop round the selected model elements
            foreach(ModelElement selectedElement in selectedMels) //(PresentationElement pel in selectedPels)
            {
                //// Get the underlying model element
                //ModelElement selectedElement = pel.ModelElement;
                // Not interested in links
                if(selectedElement == null || selectedElement is ElementLink)
                {
                    continue;
                }

                // Loop round each role the element could be involved in
                foreach(DomainRoleInfo role in selectedElement.GetDomainClass().AllDomainRolesPlayed)
                {
                    Guid domainRelId = role.DomainRelationship.Id;

                    // Ignore PresentationViewsSubject roles
                    if(domainRelId == PresentationViewsSubject.DomainClassId)
                    {
                        continue;
                    }

                    // Loop round all of the other elements that this element is linked
                    // to via this relationship
                    foreach(ModelElement linkedElement in role.GetLinkedElements(selectedElement))
                    {
                        //HACK: assume that everything we link to is a storyboard element
                        // Only interested in storyboard element
                        StoryboardElement linkedConcept = linkedElement as StoryboardElement;

                        if(linkedConcept == null)
                        {
                            continue;
                        }

                        // Have we already linked to the other element, or is
                        // it one of the selected mels?
                        if(!elementsLinkedTo.Contains(linkedConcept) &&
                            !selectedMels.Contains(linkedConcept))
                        {
                            // No - create a new link to this element.
                            newRef.Sources.Add(linkedConcept);

                            // Make sure we don't add another link to this element
                            elementsLinkedTo.Add(linkedConcept);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Returns a list of all of the MELs that are linked to by the
        /// items in the supplied collection of PELs.
        /// </summary>
        /// <param name="selectedPels"></param>
        /// <returns></returns>
        private static List<ModelElement> GetMelsInSelection(IList<ModelElement> selectedElements)
        {
            List<ModelElement> melsInSelection = new List<ModelElement>();

            foreach(ModelElement el in selectedElements)
            {
                PresentationElement pel = el as PresentationElement;

                if(pel != null && pel.ModelElement != null)
                {
                    melsInSelection.Add(pel.ModelElement);
                }
            }

            return melsInSelection;
        }

        /// <summary>
        /// Returns the NodeShape for the first non-diagram, non-link
        /// shape in the collection
        /// </summary>
        /// <param name="selectedShapes"></param>
        /// <returns></returns>
        private static NodeShape GetFirstPelInSelection(
            IList<ModelElement> selectedElements)
        {
            foreach(ModelElement el in selectedElements)
            {
                NodeShape pel = el as NodeShape;

                if(pel != null && !(pel is Diagram))
                {
                    return pel;
                }
            }

            return null;
        }
        #endregion
    }
}